﻿using IdentityServer4.EntityFramework.Entities;
using Microsoft.EntityFrameworkCore;
using Client = IdentityServer4.EntityFramework.Entities.Client;
using ClientClaim = IdentityServer4.EntityFramework.Entities.ClientClaim;
using ConfigurationDbContext = Devonline.Identity.Admin.Models.ConfigurationDbContext;

namespace Devonline.Identity.Admin;

public class ClientService
{
    private readonly ILogger<ClientService> _logger;
    private readonly ConfigurationDbContext _context;
    private readonly HttpContext _httpContext;
    private readonly HttpRequest _request;

    public ClientService(
        ILogger<ClientService> logger,
        ConfigurationDbContext context,
        IHttpContextAccessor httpContextAccessor
        )
    {
        if (httpContextAccessor.HttpContext == null)
        {
            throw new ArgumentNullException(nameof(httpContextAccessor.HttpContext), "HttpContext can't be null!");
        }

        _logger = logger;
        _context = context;
        _httpContext = httpContextAccessor.HttpContext;
        _request = _httpContext.Request;
    }

    public virtual Task<Client> GetClientAsync(int clientId)
    {
        return _context.Clients
            .Include(x => x.AllowedGrantTypes)
            .Include(x => x.RedirectUris)
            .Include(x => x.PostLogoutRedirectUris)
            .Include(x => x.AllowedScopes)
            .Include(x => x.ClientSecrets)
            .Include(x => x.Claims)
            .Include(x => x.IdentityProviderRestrictions)
            .Include(x => x.AllowedCorsOrigins)
            .Include(x => x.Properties)
            .Where(x => x.Id == clientId)
            .AsNoTracking()
            .SingleOrDefaultAsync();
    }

    public virtual async Task<PagedResult<Client>> GetClientsAsync(string client = "") => await _context.Clients.WhereIf(!string.IsNullOrWhiteSpace(client), x => x.ClientName.Contains(client)).PageByAsync(_request.GetPagedRequest());

    public virtual async Task<List<string>> GetScopesAsync(string scope, int limit = 0)
    {
        var identityResources = await _context.IdentityResources
            .WhereIf(!string.IsNullOrEmpty(scope), x => x.Name.Contains(scope))
            .Select(x => x.Name).ToListAsync();

        var apiScopes = await _context.ApiScopes
            .WhereIf(!string.IsNullOrEmpty(scope), x => x.Name.Contains(scope))
            .Select(x => x.Name).ToListAsync();

        var scopes = identityResources.Concat(apiScopes).ToList();

        return scopes;
    }

    public virtual List<string> GetGrantTypes(string grant)
    {
        var filteredGrants = ClientConsts.GetGrantTypes().AsQueryable()
            .WhereIf(!string.IsNullOrWhiteSpace(grant), x => x.Contains(grant))
            .ToList();

        return filteredGrants;
    }

    public virtual List<string> GetSigningAlgorithms(string algorithm)
    {
        var signingAlgorithms = ClientConsts.SigningAlgorithms().AsQueryable()
            .WhereIf(!string.IsNullOrWhiteSpace(algorithm), x => x.Contains(algorithm))
            .OrderBy(x => x)
            .ToList();

        return signingAlgorithms;
    }

    public virtual List<string> GetSecretTypes() => ClientConsts.GetSecretTypes();

    public virtual List<string> GetStandardClaims(string claim, int limit = 0)
    {
        var filteredClaims = ClientConsts.GetStandardClaims().AsQueryable()
            .WhereIf(!string.IsNullOrWhiteSpace(claim), x => x.Contains(claim))
            .ToList();

        return filteredClaims;
    }

    public virtual async Task<int> AddClientSecretAsync(int clientId, ClientSecret clientSecret)
    {
        var client = await _context.Clients.Where(x => x.Id == clientId).SingleOrDefaultAsync();
        clientSecret.Client = client;

        await _context.ClientSecrets.AddAsync(clientSecret);

        return await _context.SaveChangesAsync();
    }

    public virtual Task<ClientProperty> GetClientPropertyAsync(int clientPropertyId)
    {
        return _context.ClientProperties
            .Include(x => x.Client)
            .Where(x => x.Id == clientPropertyId)
            .SingleOrDefaultAsync();
    }

    public virtual async Task<int> AddClientClaimAsync(int clientId, ClientClaim clientClaim)
    {
        var client = await _context.Clients.Where(x => x.Id == clientId).SingleOrDefaultAsync();

        clientClaim.Client = client;
        await _context.ClientClaims.AddAsync(clientClaim);

        return await _context.SaveChangesAsync();
    }

    public virtual async Task<int> AddClientPropertyAsync(int clientId, ClientProperty clientProperty)
    {
        var client = await _context.Clients.Where(x => x.Id == clientId).SingleOrDefaultAsync();

        clientProperty.Client = client;
        await _context.ClientProperties.AddAsync(clientProperty);

        return await _context.SaveChangesAsync();
    }

    public virtual async Task<(string ClientId, string ClientName)> GetClientIdAsync(int clientId)
    {
        var client = await _context.Clients.Where(x => x.Id == clientId)
            .Select(x => new { x.ClientId, x.ClientName })
            .SingleOrDefaultAsync();

        return (client?.ClientId, client?.ClientName);
    }

    public virtual async Task<PagedResult<ClientSecret>> GetClientSecretsAsync(int clientId) => await _context.ClientSecrets.Where(x => x.Client.Id == clientId).PageByAsync(_request.GetPagedRequest());

    public virtual Task<ClientSecret> GetClientSecretAsync(int clientSecretId)
    {
        return _context.ClientSecrets
            .Include(x => x.Client)
            .Where(x => x.Id == clientSecretId)
            .AsNoTracking()
            .SingleOrDefaultAsync();
    }

    public virtual async Task<PagedResult<ClientClaim>> GetClientClaimsAsync(int clientId) => await _context.ClientClaims.Where(x => x.Client.Id == clientId).PageByAsync(_request.GetPagedRequest());

    public virtual async Task<PagedResult<ClientProperty>> GetClientPropertiesAsync(int clientId) => await _context.ClientProperties.Where(x => x.Client.Id == clientId).PageByAsync(_request.GetPagedRequest());

    public virtual Task<ClientClaim> GetClientClaimAsync(int clientClaimId)
    {
        return _context.ClientClaims
            .Include(x => x.Client)
            .Where(x => x.Id == clientClaimId)
            .AsNoTracking()
            .SingleOrDefaultAsync();
    }

    public virtual async Task<int> DeleteClientSecretAsync(ClientSecret clientSecret)
    {
        var secretToDelete = await _context.ClientSecrets.Where(x => x.Id == clientSecret.Id).SingleOrDefaultAsync();

        _context.ClientSecrets.Remove(secretToDelete);

        return await _context.SaveChangesAsync();
    }

    public virtual async Task<int> DeleteClientClaimAsync(ClientClaim clientClaim)
    {
        var claimToDelete = await _context.ClientClaims.Where(x => x.Id == clientClaim.Id).SingleOrDefaultAsync();

        _context.ClientClaims.Remove(claimToDelete);
        return await _context.SaveChangesAsync();
    }

    public virtual async Task<int> DeleteClientPropertyAsync(ClientProperty clientProperty)
    {
        var propertyToDelete = await _context.ClientProperties.Where(x => x.Id == clientProperty.Id).SingleOrDefaultAsync();

        _context.ClientProperties.Remove(propertyToDelete);
        return await _context.SaveChangesAsync();
    }

    public virtual async Task<int> SaveAllChangesAsync()
    {
        return await _context.SaveChangesAsync();
    }

    public virtual async Task<bool> CanInsertClientAsync(Client client, bool isCloned = false)
    {
        if (client.Id == 0 || isCloned)
        {
            var existsWithClientName = await _context.Clients.Where(x => x.ClientId == client.ClientId).SingleOrDefaultAsync();
            return existsWithClientName == null;
        }
        else
        {
            var existsWithClientName = await _context.Clients.Where(x => x.ClientId == client.ClientId && x.Id != client.Id).SingleOrDefaultAsync();
            return existsWithClientName == null;
        }
    }

    /// <summary>
    /// Add new client, this method doesn't save client secrets, client claims, client properties
    /// </summary>
    /// <param name="client"></param>
    /// <returns>This method return new client id</returns>
    public virtual async Task<int> AddClientAsync(Client client)
    {
        _context.Clients.Add(client);

        await _context.SaveChangesAsync();

        return client.Id;
    }

    public virtual async Task<int> CloneClientAsync(Client client,
        bool cloneClientCorsOrigins = true,
        bool cloneClientGrantTypes = true,
        bool cloneClientIdPRestrictions = true,
        bool cloneClientPostLogoutRedirectUris = true,
        bool cloneClientScopes = true,
        bool cloneClientRedirectUris = true,
        bool cloneClientClaims = true,
        bool cloneClientProperties = true
        )
    {
        var clientToClone = await _context.Clients
            .Include(x => x.AllowedGrantTypes)
            .Include(x => x.RedirectUris)
            .Include(x => x.PostLogoutRedirectUris)
            .Include(x => x.AllowedScopes)
            .Include(x => x.ClientSecrets)
            .Include(x => x.Claims)
            .Include(x => x.IdentityProviderRestrictions)
            .Include(x => x.AllowedCorsOrigins)
            .Include(x => x.Properties)
            .AsNoTracking()
            .FirstOrDefaultAsync(x => x.Id == client.Id);

        clientToClone.ClientName = client.ClientName;
        clientToClone.ClientId = client.ClientId;

        //Clean original ids
        clientToClone.Id = 0;
        clientToClone.AllowedCorsOrigins.ForEach(x => x.Id = 0);
        clientToClone.RedirectUris.ForEach(x => x.Id = 0);
        clientToClone.PostLogoutRedirectUris.ForEach(x => x.Id = 0);
        clientToClone.AllowedScopes.ForEach(x => x.Id = 0);
        clientToClone.ClientSecrets.ForEach(x => x.Id = 0);
        clientToClone.IdentityProviderRestrictions.ForEach(x => x.Id = 0);
        clientToClone.Claims.ForEach(x => x.Id = 0);
        clientToClone.AllowedGrantTypes.ForEach(x => x.Id = 0);
        clientToClone.Properties.ForEach(x => x.Id = 0);

        //Client secret will be skipped
        clientToClone.ClientSecrets.Clear();

        if (!cloneClientCorsOrigins)
        {
            clientToClone.AllowedCorsOrigins.Clear();
        }

        if (!cloneClientGrantTypes)
        {
            clientToClone.AllowedGrantTypes.Clear();
        }

        if (!cloneClientIdPRestrictions)
        {
            clientToClone.IdentityProviderRestrictions.Clear();
        }

        if (!cloneClientPostLogoutRedirectUris)
        {
            clientToClone.PostLogoutRedirectUris.Clear();
        }

        if (!cloneClientScopes)
        {
            clientToClone.AllowedScopes.Clear();
        }

        if (!cloneClientRedirectUris)
        {
            clientToClone.RedirectUris.Clear();
        }

        if (!cloneClientClaims)
        {
            clientToClone.Claims.Clear();
        }

        if (!cloneClientProperties)
        {
            clientToClone.Properties.Clear();
        }

        await _context.Clients.AddAsync(clientToClone);

        await _context.SaveChangesAsync();

        var id = clientToClone.Id;

        return id;
    }

    private async Task RemoveClientRelationsAsync(Client client, bool updateClientClaims,
        bool updateClientProperties)
    {
        //Remove old allowed scopes
        var clientScopes = await _context.ClientScopes.Where(x => x.Client.Id == client.Id).ToListAsync();
        _context.ClientScopes.RemoveRange(clientScopes);

        //Remove old grant types
        var clientGrantTypes = await _context.ClientGrantTypes.Where(x => x.Client.Id == client.Id).ToListAsync();
        _context.ClientGrantTypes.RemoveRange(clientGrantTypes);

        //Remove old redirect uri
        var clientRedirectUris = await _context.ClientRedirectUris.Where(x => x.Client.Id == client.Id).ToListAsync();
        _context.ClientRedirectUris.RemoveRange(clientRedirectUris);

        //Remove old client cors
        var clientCorsOrigins = await _context.ClientCorsOrigins.Where(x => x.Client.Id == client.Id).ToListAsync();
        _context.ClientCorsOrigins.RemoveRange(clientCorsOrigins);

        //Remove old client id restrictions
        var clientIdPRestrictions = await _context.ClientIdPRestrictions.Where(x => x.Client.Id == client.Id).ToListAsync();
        _context.ClientIdPRestrictions.RemoveRange(clientIdPRestrictions);

        //Remove old client post logout redirect
        var clientPostLogoutRedirectUris = await _context.ClientPostLogoutRedirectUris.Where(x => x.Client.Id == client.Id).ToListAsync();
        _context.ClientPostLogoutRedirectUris.RemoveRange(clientPostLogoutRedirectUris);

        //Remove old client claims
        if (updateClientClaims)
        {
            var clientClaims = await _context.ClientClaims.Where(x => x.Client.Id == client.Id).ToListAsync();
            _context.ClientClaims.RemoveRange(clientClaims);
        }

        //Remove old client properties
        if (updateClientProperties)
        {
            var clientProperties = await _context.ClientProperties.Where(x => x.Client.Id == client.Id).ToListAsync();
            _context.ClientProperties.RemoveRange(clientProperties);
        }
    }

    public virtual async Task<int> UpdateClientAsync(Client client, bool updateClientClaims = false, bool updateClientProperties = false)
    {
        //Remove old relations
        await RemoveClientRelationsAsync(client, updateClientClaims, updateClientProperties);

        //Update with new data
        _context.Clients.Update(client);

        return await _context.SaveChangesAsync();
    }

    public virtual async Task<int> RemoveClientAsync(Client client)
    {
        _context.Clients.Remove(client);

        return await _context.SaveChangesAsync();
    }
}